/**
 * Copyright (c) 2009 Cerner Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Cerner Corporation - initial API and implementation
 */
package com.cerner.system.rest.jaas.cas;

import java.net.URI;

/**
 * Thread-safe representation of the credentials for a single user that would
 * like to authenticate against the centralized authentication service.
 * 
 * @author Alex Horn
 * @since 1.0.0
 */
public final class CasCredential implements javax.security.auth.Destroyable {

  /**
   * HTTP-specific component that is responsible for the generation of CAS 1.0
   * service tickets. We synchronize on it with "{@code this}" intrinsic lock.
   */
  private final CasTicketConnector casTicketConnector;

  /**
   * User name with which to sign in to CAS.
   */
  private final String username;

  /**
   * Password with which validate the {@link #username}. This field is non-final
   * in order to make it eligible for garbage collection.
   */
  private char[] password;

  /**
   * Flag that indicates if an {@link IllegalStateException} should be thrown.
   * 
   * @see CasCredential#destroy()
   */
  private boolean isDestroyed;

  /**
   * Internal constructor to instantiate a new {@link CasCredential}. Instances
   * of this class must be added to the private credential set of a
   * {@link javax.security.auth.Subject}.
   * 
   * @param casTicketConnector HTTP connector to create new CAS service tickets
   * @param username user identifier of the entity that would like to connect to
   *          the server
   * @param password password information used in the user authentication
   *          process
   */
  CasCredential(CasTicketConnector casTicketConnector, String username,
      char[] password) {
    this.casTicketConnector = casTicketConnector;
    this.username = username;

    /* defensively copy the provided password */
    this.password = password.clone();
  }

  /**
   * Get the URI of the centralized authentication service with which a TGT has
   * been registered.
   * 
   * @return URL of CAS login service
   * @throws IllegalStateException if the credential has been {@link #destroy()
   *           destroyed}
   */
  public synchronized URI getCasLoginUrl() {
    checkDestroyed();

    return _getCasLoginUrl();
  }

  /**
   * Get the username that must be used to authenticate against CAS.
   * 
   * @return user identifier used during authentication to CAS
   * @throws IllegalStateException if the credential has been {@link #destroy()
   *           destroyed}
   */
  public synchronized String getUsername() {
    checkDestroyed();

    return _getUsername();
  }

  /**
   * Get a copy of the password that must be used to authenticate against CAS.
   * 
   * @return password used during authentication to CAS
   * @throws IllegalStateException if the credential has been {@link #destroy()
   *           destroyed}
   */
  public synchronized char[] getPassword() {
    checkDestroyed();

    return _getPassword();
  }

  /**
   * Create a new CAS 1.0 service ticket.
   * 
   * @throws IllegalStateException if the credential has been {@link #destroy()
   *           destroyed}
   * @throws CasTicketException if the CAS ticket granting ticket expired or was
   *           invalidated on the server-side
   * @throws CasFault if an unrecoverable error occurred
   */
  public synchronized String getServiceTicket(URI service)
      throws CasTicketException {
    checkDestroyed();

    try {
      return this.casTicketConnector.create(service);
    } catch (CasConnectorException e) {
      throw new CasTicketException(e);
    } catch (CasConnectorFault e) {
      throw new CasFault("Unable to create CAS service ticket.", e);
    }
  }

  /**
   * Compare two {@link CasCredential} objects for equality. They are equal if
   * and only if their {@link #getCasLoginUrl() casLoginUrl},
   * {@link #getUsername() user identifier} and {@link #getPassword() password}
   * information match.
   * 
   * @return boolean {@code true} if the {@code casLoginUrl}, user name and
   *         password match, {@code false} otherwise
   */
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    } else if (object instanceof CasCredential) {

      /*
       * Only use the _get*() methods to avoid an inconsistent behavior of
       * equals(...) due to the possibility of an IllegalStateException on
       * public getters.
       */
      final CasCredential other = (CasCredential) (object);

      if (!_getUsername().equals(other._getUsername())) {
        return false;
      } else if (!_getCasLoginUrl().equals(other._getCasLoginUrl())) {
        return false;
      }

      /*
       * The destroy flag and password char[] must be retrieved atomically to
       * guarantee correctness. However, synchronization on both object must be
       * done independently to avoid deadlock.
       */

      final boolean thisDestroyed;
      final char[] thisPassword;
      synchronized (this) {
        thisDestroyed = isDestroyed();
        thisPassword = _getPassword();
      }

      final boolean otherDestroyed;
      final char[] otherPassword;
      synchronized (other) {
        otherDestroyed = other.isDestroyed();
        otherPassword = other._getPassword();
      }

      if (thisDestroyed == otherDestroyed) {
        if (!thisDestroyed) {
          /* both credentials are active */
          return java.util.Arrays.equals(thisPassword, otherPassword);
        } else {
          /*
           * both credentials have been destroyed so their passwords are
           * meaningless
           */
          return true;
        }
      } else {
        return false;
      }
    } else {
      return false;
    }
  }

  /**
   * Compute the hash code from the {@code casLoginUrl} and
   * {@link #getUsername() user identifier} .
   * <p>
   * Recall, the hash code for two unequal objects can be the same. Such a
   * scenario is unlikely to occur with credentials because it requires the
   * existence of two {@link CasCredential} objects that only differ by the
   * user's password. It is handled gracefully, however.
   */
  @Override
  public int hashCode() {
    return _getCasLoginUrl().hashCode() + _getUsername().hashCode();
  }

  /**
   * Internal helper method to fulfill the
   * {@link javax.security.auth.Destroyable#destroy()} contract. Explicit
   * synchronization on "this" intrinsic lock is recommended when referencing
   * other object state after the check has succeeded.
   * 
   * @throws IllegalStateException if the credential has been {@link #destroy()
   *           destroyed}
   */
  void checkDestroyed() {
    if (isDestroyed()) {
      throw new IllegalStateException("CasCredential has been destroyed.");
    }
  }

  /**
   * Internal API to obtain the URI of the centralized authentication service
   * with which a TGT has been registered without performing an
   * {@link #isDestroyed()} check.
   * 
   * @return never {@code null}
   */
  java.net.URI _getCasLoginUrl() {
    return this.casTicketConnector.getCasLoginUrl();
  }

  /**
   * Internal API to obtain the user identifier that must be used to
   * authenticate against CAS without performing an {@link #isDestroyed()}
   * check.
   * 
   * @return never {@code null}
   */
  String _getUsername() {
    return this.username;
  }

  /**
   * Internal API to obtain a copy of the password that must be used to
   * authenticate against CAS performing an {@link #isDestroyed()} check.
   * Explicit synchronization on "this" intrinsic lock is required.
   * <p>
   * This method returns {@code null} once the credential has been
   * {@link #destroy() destroyed}.
   * 
   * @return password which may be {@code null} after credential has been
   *         {@link #destroy() destroyed}
   */
  char[] _getPassword() {
    return this.password == null ? null : this.password.clone();
  }

  /**
   * Clears and re-allocates the memory of the password data structure. Any
   * subsequent invocation of <em>public</em> getter methods will trigger an
   * {@link IllegalStateException}.
   * <p>
   * This method exhibits an idempotent behavior.
   */
  public synchronized void destroy() {
    java.util.Arrays.fill(this.password, ' ');

    // make the password eligible for garbage collection
    this.password = null;

    // subsequent calls to this object must trigger an IllegalStateException
    this.isDestroyed = true;
  }

  /**
   * Determine if the password data structure has been {@link #destroy()
   * destroyed}.
   * 
   * @return boolean {@code true} if the password has been cleared, {@code
   *         false} if all user authentication information is intact
   */
  public synchronized boolean isDestroyed() {
    return this.isDestroyed;
  }

  /**
   * Obtain a debugger-friendly representation of this CAS credential.
   */
  @Override
  public String toString() {
    final StringBuilder builder = new StringBuilder();

    builder.append("CasCredential[cas=\""); //$NON-NLS-1$
    builder.append(_getCasLoginUrl());
    builder.append("\", user=\""); //$NON-NLS-1$
    builder.append(_getUsername());
    builder.append("\"]"); //$NON-NLS-1$

    return builder.toString();
  }

}
